﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Logic.Functions;

namespace Logic
{
    public class KohonenLayer : BaseLayer
    {
        public enum NeighbourhoodType
        {
            OneDimensional,
            Square,
            Diamond
        }

        public Neuron[][] Neurons { get; private set; }
        private float[][] winningFreq;

        public override string Symbol
        {
            get { return "KohonenLayer"; }
        }

        public KohonenLayer()
        {
        }

        public KohonenLayer(int rows, int columns, int prevLayerSize)
        {
            CreateNeurons(rows, columns, prevLayerSize);
        }

        public override void ProcessNeurons(float[] prevLayerSignals)
        {
            ProcessNeurons(Normalize(prevLayerSignals), false, 0);
        }

        public void ProcessNeurons(float[] prevLayerSignals, bool trainingMode, float conscienceFactor)
        {
            float[] signals = Normalize(prevLayerSignals);
            float min = float.MaxValue;
            Neuron chosenOne = null;
            for (int i = 0; i < Neurons.Length; i++)
                for (int j = 0; j < Neurons[0].Length; j++ )
                {
                    Neurons[i][j].EvaluateEuclideanWithConscience(ActivationFunction, signals, conscienceFactor, Neurons.Length, trainingMode ? winningFreq[i][j] : 0);
                    if (Neurons[i][j].OutputSignal < min)
                    {
                        min = Neurons[i][j].OutputSignal;
                        chosenOne = Neurons[i][j];
                    }
                    Neurons[i][j].OutputSignal = 0;
                }
            chosenOne.OutputSignal = 1;
        }

        public override float[] GetOutputSignals()
        {
            var outputSignals = new float[Size];
            for (int i = 0; i < Neurons.Length; i++)
                for (int j = 0; j < Neurons[0].Length; j++)
                outputSignals[i*Neurons[0].Length + j] = Neurons[i][j].OutputSignal;
            return outputSignals;
        }

        public override void Save(StreamWriter sw)
        {
            sw.WriteLine(Symbol);
            sw.WriteLine("{0} {1} {2}", Neurons.Length, Neurons[0].Length, ActivationFunction.Symbol);
            foreach (Neuron[] rows in Neurons)
            {
                foreach (var neuron in rows)
                {
                    foreach (float w in neuron.Weights)
                        sw.Write("{0} ", w);
                    sw.WriteLine();
                }
            }
        }

        public override void CleanWeights()
        {
            foreach (Neuron[] row in Neurons)
                foreach (Neuron n in row)
                    n.CleanWeights();
        }

        public override void InitializeWeights(float min, float max)
        {
            foreach (Neuron[] row in Neurons)
                foreach (Neuron n in row)
                    n.InitializeWeights(min, max);
        }

        public void CreateNeurons(int rows, int columns, int prevLayerSize)
        {
            if (rows < 1 || columns < 1 || prevLayerSize < 1)
                throw new ArgumentException("Sizes must be positive.");
            Neurons = new Neuron[rows][];
            for (int i=0; i<rows; i++)
            {
                Neurons[i] = new Neuron[columns];
                for (int j=0; j<columns; j++)
                    Neurons[i][j] = new Neuron(prevLayerSize);
            }
        }

        public void Learn(int steps, float learningFactor, NeighbourhoodType typeOfNeighbourhood, float neighbourhoodWidth, float conscienceFactor, float[][] trainingSet)
        {
            InitWinningFreq();
            for (int i = 0; i < steps; i++)
                DoLearnStep(learningFactor, typeOfNeighbourhood, neighbourhoodWidth, conscienceFactor, trainingSet);
        }

        public override void PrintWeights()
        {
            
            foreach (Neuron[] rows in Neurons)
            {
                foreach (var neuron in rows)
                {
                    for (int i = 0; i<neuron.Weights.Length; i++ )
                    {
                        Console.Write("{0} ", neuron.Weights[i]);
                        if (i%3 == 2)
                            Console.WriteLine();
                    }
                    Console.WriteLine();
                }
            }
        }

        public override int Size
        {
            get { return Neurons.Length*Neurons[0].Length; }
        }

        protected float[] Normalize(float[] prevLayerSignals)
        {
            float sum = prevLayerSignals.Sum(f => f >= 0 ? f : -f);
            if (sum.Equals(0))
                return prevLayerSignals;
            var normalized = new float[prevLayerSignals.Length];
            for (int i = 0; i < normalized.Length; i++)
                normalized[i] = prevLayerSignals[i] / sum;
            return normalized;
        }

        protected void DoLearnStep(float learningFactor, NeighbourhoodType typeOfNeighbourhood, float neighbourhoodWidth, float conscienceFactor, float[][] trainingSet)
        {
            float beta = 1.0f/trainingSet.Length;
            foreach (var signals in trainingSet)
                Train(signals, learningFactor, typeOfNeighbourhood, neighbourhoodWidth, conscienceFactor, beta);
        }

        private void Train(float[] signals, float learningFactor, NeighbourhoodType typeOfNeighbourhood, float neighbourhoodWidth, float conscienceFactor, float beta)
        {
            ProcessNeurons(signals, true, conscienceFactor);
            for (int i = 0; i < Neurons.Length; i++)
                for (int j = 0; j < Neurons[0].Length; j++)
                    if (Neurons[i][j].OutputSignal.Equals(1))
                    {
                        UpdateWinnerAndNeighbourhood(i, j, learningFactor, typeOfNeighbourhood, neighbourhoodWidth, signals);
                        UpdateWinningFreq(i, j, beta);
                        return;
                    }
        }

        private void InitWinningFreq()
        {
            float layerSize = Neurons.Length * Neurons[0].Length;
            winningFreq = new float[Neurons.Length][];
            for (int i = 0; i < Neurons.Length; i++)
            {
                winningFreq[i] = new float[Neurons[0].Length];
                for (int j = 0; j < Neurons[0].Length; j++)
                    winningFreq[i][j] = 1.0f / layerSize;
            }
        }

        private void UpdateWinningFreq(int row, int col, float beta)
        {
            for (int i=0; i<Neurons.Length; i++)
                for (int j=0; j<Neurons[0].Length; j++)
                    winningFreq[i][j] -= beta*winningFreq[i][j];
            winningFreq[row][col] += beta;
        }

        private void UpdateWinnerAndNeighbourhood(int row, int column, float learningFactor, NeighbourhoodType typeOfNeighbourhood, float neighbourhoodWidth, float[] signals)
        {
            for (int i = 0; i < Neurons[row][column].Weights.Length; i++)
                Neurons[row][column].Weights[i] += learningFactor*(signals[i] - Neurons[row][column].Weights[i]);
            for (int i=0; i<Neurons.Length; i++)
                for (int j=0; j<Neurons[0].Length; j++)
                {
                    int distance = ComputeDistanceToWinner(row, column, i, j, Neurons.Length, typeOfNeighbourhood);
                    if (distance <= neighbourhoodWidth && distance > 0)
                        UpdateWeights(Neurons[i][j].Weights, learningFactor, distance, signals);
                }
        }

        private void UpdateWeights(float[] weights, float learningFactor, int distanceToWinner, float[] signals)
        {
            for (int i = 0; i < weights.Length; i++)
                weights[i] += (float)(learningFactor*ComputeTopologicalNeighborhood(distanceToWinner)*(signals[i] - weights[i]));
        }

        private double ComputeTopologicalNeighborhood(int distanceToWinner)
        {
            return Math.Exp(-distanceToWinner*distanceToWinner);
        }

        private int ComputeDistanceToWinner(int winnerRow, int winnerColumn, int currentRow, int currentColumn, int columnsInRow, NeighbourhoodType typeOfNeighbourhood)
        {
            int distance;
            if (typeOfNeighbourhood.Equals(NeighbourhoodType.OneDimensional))
                distance = Mod((winnerRow - currentRow)*columnsInRow + winnerColumn - currentColumn);
            else
            {
                int rowsDiff = Mod(winnerRow - currentRow);
                int columnsDiff = Mod(winnerColumn - currentColumn);
                if (typeOfNeighbourhood.Equals(NeighbourhoodType.Square))
                    distance = Math.Max(rowsDiff, columnsDiff);
                else // diamond
                    distance = rowsDiff + columnsDiff;
            }
            return distance;
        }

        private static int Mod(int x)
        {
            return x > 0 ? x : -x;
        }
    }
}
